
#ifdef ENABLE_PLENGINE_PYTHON

#include "python_vm_hotfix.h"
#include "python_vm.h"
#include "program_options.h"
#include "io.h"

std::string python_reloader_string = R"(

# Python Module Reloader
#
# Copyright (c) 2009-2015 Jon Parise <jon@indelible.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Python Module Reloader"""

try:
    import builtins
except ImportError:
    import __builtin__ as builtins

import imp
import sys
import types

__author__ = 'Jon Parise <jon@indelible.org>'
__version__ = '0.6'

__all__ = ('enable', 'disable', 'get_dependencies', 'reload')

_baseimport = builtins.__import__
_blacklist = []
_dependencies = dict()
_parent = None

# Jython doesn't have imp.reload().
if not hasattr(imp, 'reload'):
    imp.reload = reload

# PEP 328 changed the default level to 0 in Python 3.3.
_default_level = -1 if sys.version_info < (3, 3) else 0

def enable(blacklist=None):
    """Enable global module dependency tracking.

    A blacklist can be specified to exclude specific modules (and their import
    hierachies) from the reloading process.  The blacklist can be any iterable
    listing the fully-qualified names of modules that should be ignored.  Note
    that blacklisted modules will still appear in the dependency graph; they
    will just not be reloaded.
    """
    global _blacklist
    builtins.__import__ = _import
    if blacklist is not None:
        _blacklist = frozenset(blacklist)

def disable():
    """Disable global module dependency tracking."""
    global _blacklist, _parent
    builtins.__import__ = _baseimport
    _blacklist = None
    _dependencies.clear()
    _parent = None

def get_dependencies(m):
    """Get the dependency list for the given imported module."""
    name = m.__name__ if isinstance(m, types.ModuleType) else m
    return _dependencies.get(name, None)

def _deepcopy_module_dict(m):
    """Make a deep copy of a module's dictionary."""
    import copy
    import types

    # We can't deepcopy() everything in the module's dictionary because some
    # items, such as '__builtins__', aren't deepcopy()-able.  To work around
    # that, we start by making a shallow copy of the dictionary, giving us a
    # way to remove keys before performing the deep copy.
    d = vars(m).copy()
    del d['__builtins__']
    tmp = []
    cobjs = {}
    for k, v in d.items():
        if type(v) == types.ModuleType:
            tmp.append(k)
    for t in tmp:
        del d[t]
    return d

def _reload(m, visited, t):
    """Internal module reloading routine."""
    name = m.__name__

    # If this module's name appears in our blacklist, skip its entire
    # dependency hierarchy.
    if _blacklist and name in _blacklist:
        return

    # Start by adding this module to our set of visited modules.  We use this
    # set to avoid running into infinite recursion while walking the module
    # dependency graph.
    visited.add(m)

    # Start by reloading all of our dependencies in reverse order.  Note that
    # we recursively call ourself to perform the nested reloads.
    deps = _dependencies.get(name, None)
    if deps is not None:
        for dep in reversed(deps):
            if dep not in visited:
                _reload(dep, visited, t)

    # Clear this module's list of dependencies.  Some import statements may
    # have been removed.  We'll rebuild the dependency list as part of the
    # reload operation below.
    try:
        del _dependencies[name]
    except KeyError:
        pass

    # Because we're triggering a reload and not an import, the module itself
    # won't run through our _import hook below.  In order for this module's
    # dependencies (which will pass through the _import hook) to be associated
    # with this module, we need to set our parent pointer beforehand.
    global _parent
    _parent = name

    # If the module has a __reload__(d) function, we'll call it with a copy of
    # the original module's dictionary after it's been reloaded.
    callback = getattr(m, '__reload__', None)
    if callback is not None:
        if t == 1:
            d = _deepcopy_module_dict(m)
            imp.reload(m)
            callback(d)
    else:
        if t == 0:
            imp.reload(m)

    # Reset our parent pointer now that the reloading operation is complete.
    _parent = None

def myreload(m, t):
    """Reload an existing module.

    Any known dependencies of the module will also be reloaded.

    If a module has a __reload__(d) function, it will be called with a copy of
    the original module's dictionary after the module is reloaded."""
    _reload(m, set(), t)

def _import(name, globals=None, locals=None, fromlist=None, level=_default_level):
    """__import__() replacement function that tracks module dependencies."""
    # Track our current parent module.  This is used to find our current place
    # in the dependency graph.
    global _parent
    parent = _parent
    _parent = name

    # Perform the actual import work using the base import function.
    base = _baseimport(name, globals, locals, fromlist, level)

    if base is not None and parent is not None:
        m = base

        # We manually walk through the imported hierarchy because the import
        # function only returns the top-level package reference for a nested
        # import statement (e.g. 'package' for `import package.module`) when
        # no fromlist has been specified.  It's possible that the package
        # might not have all of its descendents as attributes, in which case
        # we fall back to using the immediate ancestor of the module instead.
        if fromlist is None:
            for component in name.split('.')[1:]:
                try:
                    m = getattr(m, component)
                except AttributeError:
                    m = sys.modules[m.__name__ + '.' + component]

        # If this is a nested import for a reloadable (source-based) module,
        # we append ourself to our parent's dependency list.
        if hasattr(m, '__file__'):
            l = _dependencies.setdefault(parent, [])
            l.append(m)

    # Lastly, we always restore our global _parent pointer.
    _parent = parent

    return base

)";

void load_hotfix()
{
    std::string files;
    std::vector<std::string> vtfiles;
    get_filenames(ProgramOptions::get_string("py_script_path"), { ".py" }, vtfiles, true);
    for (size_t i = 0; i < vtfiles.size(); i++)
    {
        if (files != "")
        {
            files += ",";
        }
        files += "'" + vtfiles[i] + "'";
    }
    std::string cmd = "def get_paths(): return [" + files + "]";
    PythonVM::exec(cmd);
    PythonVM::exec(python_reloader_string);

    std::string hotfix = R"(
import time, os, sys
import pl
import types
def init_hotfix():
    for mod in get_paths():
        try:
            module = sys.modules[mod]
        except Exception, e:
            pl.error('#1 %s:%s' % (str(Exception), str(e)))
            continue
        filename = module.__file__
        if filename.endswith('.pyc'):
            filename = filename.replace('.pyc', '.py')
        mod_time = os.path.getmtime(filename)
        if not 'loadtime' in module.__dict__:
            module.loadtime = mod_time
    if "global_variables" in sys.modules:
        sys.modules["global_variables"].__reload__ = my__reload__

def hotfix():
    need_reload = False
    for mod in get_paths():
        try:
            module = sys.modules[mod]
        except Exception, e:
            pl.error('#2 %s:%s' % (str(Exception), str(e)))
            continue
        filename = module.__file__
        if filename.endswith('.pyc'):
            filename = filename.replace('.pyc', '.py')
        mod_time = os.path.getmtime(filename)
        if not 'loadtime' in module.__dict__:
            module.loadtime = 0
        if mod_time > module.loadtime:
            need_reload = True
            module.loadtime = mod_time
    if need_reload == True:
        global ___g_tmp_already_load___
        ___g_tmp_already_load___ = {}
        for mod in get_paths():
            module = sys.modules[mod]
            try:
                filename = module.__file__.replace('.pyc', '.py')
                pl.warn('reload file, file: %s' % filename)
                myreload(module, 0)
            except Exception, e:
                pl.error('#3 %s:%s' % (str(Exception), str(e)))
        for mod in get_paths():
            module = sys.modules[mod]
            try:
                myreload(module, 1)
            except Exception, e:
                pl.error('#4 %s:%s' % (str(Exception), str(e)))
        __main_reload__()
        ___g_tmp_already_load___ = {}
___g_tmp_already_load___ = {}
def check_already_load(v):
    global ___g_tmp_already_load___
    bcheck = False
    if type(v) == types.InstanceType:
        bcheck = True
    elif my_is_python_type(type(v)) == False and my_is_python_type(type(v.__class__)) == False:
        bcheck = True
    if bcheck == False:
        return False
    else:
        return ___g_tmp_already_load___.has_key(v)
___g_variables___ = {}
def my_set_variables(key, value):
    global ___g_variables___
    ___g_variables___[key] = value
def my_del_variables(key):
    global ___g_variables___
    if ___g_variables___.has_key(key):
        del ___g_variables___[key]
def my_get_variables(key):
    global ___g_variables___
    if ___g_variables___.has_key(key):
        return ___g_variables___[key]
    else:
        return None
def my_is_python_type(t):
    return t==types.IntType \
        or t==types.TypeType \
        or t==types.CodeType \
        or t==types.BooleanType \
        or t==types.UnboundMethodType \
        or t==types.StringType \
        or t==types.BuiltinMethodType \
        or t==types.FloatType \
        or t==types.DictionaryType \
        or t==types.NotImplementedType \
        or t==types.BuiltinFunctionType \
        or t==types.DictProxyType \
        or t==types.MethodType \
        or t==types.StringTypes \
        or t==types.GeneratorType \
        or t==types.ObjectType \
        or t==types.DictType \
        or t==types.GetSetDescriptorType \
        or t==types.EllipsisType \
        or t==types.ListType \
        or t==types.TupleType \
        or t==types.ModuleType \
        or t==types.FrameType \
        or t==types.FileType \
        or t==types.LongType \
        or t==types.BufferType \
        or t==types.TracebackType \
        or t==types.ClassType \
        or t==types.InstanceType \
        or t==types.MemberDescriptorType \
        or t==types.UnicodeType \
        or t==types.SliceType \
        or t==types.ComplexType \
        or t==types.LambdaType \
        or t==types.FunctionType \
        or t==types.XRangeType \
        or t==types.NoneType
def my_restore_class_detail(realitem):
    mn = realitem.__class__.__module__
    cn = realitem.__class__.__name__
    realitem.__class__ = getattr(sys.modules[mn], cn, None)
    tmpobj = None
    try:
        tmpobj = realitem.__class__()
    except Exception, e:
        print('## %s:%s' % (mn, cn))
        print('## %s:%s' % (str(Exception), str(e)))
        return
    tmpnewattrs = dir(tmpobj)
    tmpoldattrs = dir(realitem)
    for tmpattr in tmpoldattrs:
        if tmpattr not in tmpnewattrs:
            delattr(realitem, tmpattr)
    for tmpattr in tmpnewattrs:
        if tmpattr not in tmpoldattrs:
            setattr(realitem, tmpattr, getattr(tmpobj, tmpattr, None))
def my_restore_class(reali):
    global ___g_tmp_already_load___
    if check_already_load(reali):
        return
    if type(reali) == types.InstanceType:
        ___g_tmp_already_load___[reali] = 1
        my_restore_class_detail(reali)
        for v in dir(reali):
            my_restore_class(getattr(reali, v, None))
    elif type(reali) == types.DictType:
        for _, v2 in reali.items():
            my_restore_class(v2)
    elif type(reali) == types.ListType:
        for v2 in reali:
            my_restore_class(v2)
    elif type(reali) == types.TupleType:
        for v2 in reali:
            my_restore_class(v2)
    elif type(reali) == types.FunctionType:
        global ___g_variables___
        mn = reali.__module__
        cn = reali.__name__
        for k, v in ___g_variables___.items():
            if v == reali:
               ___g_variables___[k] = getattr(sys.modules[mn], cn, None)
    elif my_is_python_type(type(reali)) == False and my_is_python_type(type(reali.__class__)) == False:
        ___g_tmp_already_load___[reali] = 1
        my_restore_class_detail(reali)
        for v in dir(reali):
            my_restore_class(getattr(reali, v, None))
def my_restore_variables(self, state):
    tmpd = vars(self).copy()
    del tmpd['__builtins__']
    tmp = []
    for k, v in tmpd.items():
        if type(v) == types.ModuleType  \
            or k[0:2] == "__"           \
            or k == "my_restore_class"  \
            or k == "my_restore_variables"  \
            or k == "loadtime":         \
            tmp.append(k)
    for t in tmp:
        del tmpd[t]
    for k, _ in tmpd.items():
        setattr(self, k, state[k])
        reali = getattr(self, k, None)
        my_restore_class(reali)
def my__reload__(state):
    self = sys.modules["global_variables"]
    my_restore_variables(self, state)
def __main_reload__():
    global ___g_variables___
    my_restore_class(___g_variables___)
)";
    PythonVM::exec(hotfix);
    PythonVM::exec("init_hotfix()");
}


void hotfix()
{
    PythonVM::exec("hotfix()");
}

#endif
